/*
 *  CesiumHeatmap.js v0.1 | Cesium Heatmap Library
 *  
 *  Works with heatmap.js v2.0.0: http://www.patrick-wied.at/static/heatmapjs/
 */
(function (window) {
	'use strict';

	function define_CesiumHeatmap() {
		var CesiumHeatmap = {
			defaults: {
				useEntitiesIfAvailable: false, //whether to use entities if a Viewer is supplied or always use an ImageryProvider
				minCanvasSize: 700,           // minimum size (in pixels) for the heatmap canvas
				maxCanvasSize: 2000,          // maximum size (in pixels) for the heatmap canvas
				radiusFactor: 60,             // data point size factor used if no radius is given (the greater of height and width divided by this number yields the used radius)
				spacingFactor: 1.5,           // extra space around the borders (point radius multiplied by this number yields the spacing)
				maxOpacity: 0.8,              // the maximum opacity used if not given in the heatmap options object
				minOpacity: 0.4,              // the minimum opacity used if not given in the heatmap options object
				blur: 0.7,                   // the blur used if not given in the heatmap options object
				gradient: {                   // the gradient used if not given in the heatmap options object
					'.0': 'blue',
                    '.2': 'green',
					'.4': 'yellow',
					'.65': 'orange',
					'.95': 'red'
				},
			}
		};

		/*  Create a CesiumHeatmap instance
		 *
		 *  cesium:  the CesiumWidget or Viewer instance
		 *  bb:      the WGS84 bounding box like {north, east, south, west}
		 *  options: a heatmap.js options object (see http://www.patrick-wied.at/static/heatmapjs/docs.html#h337-create)
		 */
		CesiumHeatmap.create = function (cesium, bb, options) {
			var instance = new CHInstance(cesium, bb, options);
			return instance;
		};

		CesiumHeatmap._getContainer = function (width, height, id) {
			var c = document.createElement("div");
			if (id) {
				c.setAttribute("id", id);
			}
			c.setAttribute("style", "width: " + width + "px; height: " + height + "px; margin: 0px; display: none;");
			document.body.appendChild(c);
			return c;
		};

		CesiumHeatmap._getImageryProvider = function (instance) {
			//var n = (new Date()).getTime();
			var d = instance._heatmap.getDataURL();
			//console.log("Create data URL: " + ((new Date()).getTime() - n));

			//var n = (new Date()).getTime();
			var imgprov = new Cesium.SingleTileImageryProvider({
				url: d,
				rectangle: instance._rectangle
			});
			//console.log("Create imageryprovider: " + ((new Date()).getTime() - n));

			/**/imgprov._tilingScheme = new Cesium.WebMercatorTilingScheme({
				rectangleSouthwestInMeters: new Cesium.Cartesian2(instance._mbounds.west, instance._mbounds.south),
				rectangleNortheastInMeters: new Cesium.Cartesian2(instance._mbounds.east, instance._mbounds.north)
			});
			/*
            imgprov._tilingScheme = new Cesium.GeographicTilingScheme({
				// rectangle: new Cesium.Rectangle(instance._mbounds.west, instance._mbounds.south,instance._mbounds.east, instance._mbounds.north)
				rectangle: instance._rectangle
			});
			*/
			return imgprov;
		};

		CesiumHeatmap._getID = function (len) {
			var text = "";
			var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

			for (var i = 0; i < ((len) ? len : 8); i++)
				text += possible.charAt(Math.floor(Math.random() * possible.length));

			return text;
		};

		var WMP = new Cesium.WebMercatorProjection();

		/*  Convert a WGS84 location into a mercator location
		 *
		 *  p: the WGS84 location like {x: lon, y: lat}
		 */
		CesiumHeatmap.wgs84ToMercator = function (p) {
			var mp = WMP.project(Cesium.Cartographic.fromDegrees(p.x, p.y));
			return {
				x: mp.x,
				y: mp.y
			};
		};

		/*  Convert a WGS84 bounding box into a mercator bounding box
		 *
		 *  bb: the WGS84 bounding box like {north, east, south, west}
		 */
		CesiumHeatmap.wgs84ToMercatorBB = function (bb) {
			var sw = WMP.project(Cesium.Cartographic.fromDegrees(bb.west, bb.south));
			var ne = WMP.project(Cesium.Cartographic.fromDegrees(bb.east, bb.north));
			return {
				north: ne.y,
				east: ne.x,
				south: sw.y,
				west: sw.x
			};
		};

		/*  Convert a mercator location into a WGS84 location
		 *
		 *  p: the mercator lcation like {x, y}
		 */
		CesiumHeatmap.mercatorToWgs84 = function (p) {
			var wp = WMP.unproject(new Cesium.Cartesian3(p.x, p.y));
			return {
				x: wp.longitude,
				y: wp.latitude
			};
		};

		/*  Convert a mercator bounding box into a WGS84 bounding box
		 *
		 *  bb: the mercator bounding box like {north, east, south, west}
		 */
		CesiumHeatmap.mercatorToWgs84BB = function (bb) {
			var sw = WMP.unproject(new Cesium.Cartesian3(bb.west, bb.south));
			var ne = WMP.unproject(new Cesium.Cartesian3(bb.east, bb.north));
			return {
				north: this.rad2deg(ne.latitude),
				east: this.rad2deg(ne.longitude),
				south: this.rad2deg(sw.latitude),
				west: this.rad2deg(sw.longitude)
			};
		};

		/*  Convert degrees into radians
		 *
		 *  d: the degrees to be converted to radians
		 */
		CesiumHeatmap.deg2rad = function (d) {
			var r = d * (Math.PI / 180.0);
			return r;
		};

		/*  Convert radians into degrees
		 *
		 *  r: the radians to be converted to degrees
		 */
		CesiumHeatmap.rad2deg = function (r) {
			var d = r / (Math.PI / 180.0);
			return d;
		};

		return CesiumHeatmap;
	}

	if (typeof(CesiumHeatmap) === 'undefined') {
		window.CesiumHeatmap = define_CesiumHeatmap();
	} else {
		console.log("CesiumHeatmap already defined.");
	}
})(window);

/*  Initiate a CesiumHeatmap instance
 *
 *  c:  CesiumWidget instance
 *  bb: a WGS84 bounding box like {north, east, south, west}
 *  o:  a heatmap.js options object (see http://www.patrick-wied.at/static/heatmapjs/docs.html#h337-create)
 */
function CHInstance(c, bb, o) {
	if (!bb) {
		return null;
	}
	if (!o) {
		o = {};
	}

	this._cesium = c;
	this._options = o;
	this._id = CesiumHeatmap._getID();

	this._options.gradient = ((this._options.gradient) ? this._options.gradient : CesiumHeatmap.defaults.gradient);
	this._options.maxOpacity = ((this._options.maxOpacity) ? this._options.maxOpacity : CesiumHeatmap.defaults.maxOpacity);
	this._options.minOpacity = ((this._options.minOpacity) ? this._options.minOpacity : CesiumHeatmap.defaults.minOpacity);
	this._options.blur = ((this._options.blur) ? this._options.blur : CesiumHeatmap.defaults.blur);

	this._mbounds = CesiumHeatmap.wgs84ToMercatorBB(bb);
	this._setWidthAndHeight(this._mbounds);

	this._options.radius = Math.round((this._options.radius) ? this._options.radius : ((this.width > this.height) ? this.width / CesiumHeatmap.defaults.radiusFactor : this.height / CesiumHeatmap.defaults.radiusFactor));

	this._spacing = this._options.radius * CesiumHeatmap.defaults.spacingFactor;
	this._xoffset = this._mbounds.west;
	this._yoffset = this._mbounds.south;

	this.width = Math.round(this.width + this._spacing * 2);
	this.height = Math.round(this.height + this._spacing * 2);

	this._mbounds.west -= this._spacing * this._factor;
	this._mbounds.east += this._spacing * this._factor;
	this._mbounds.south -= this._spacing * this._factor;
	this._mbounds.north += this._spacing * this._factor;

	this.bounds = CesiumHeatmap.mercatorToWgs84BB(this._mbounds);

	this._rectangle = Cesium.Rectangle.fromDegrees(this.bounds.west, this.bounds.south, this.bounds.east, this.bounds.north);
	this._container = CesiumHeatmap._getContainer(this.width, this.height, this._id);
	this._options.container = this._container;
	this._heatmap = h337.create(this._options)[0];
	this._container.children[0].setAttribute("id", this._id + "-hm");
}

/*  Convert a WGS84 location to the corresponding heatmap location
 *
 *  p: a WGS84 location like {x:lon, y:lat}
 */
CHInstance.prototype.wgs84PointToHeatmapPoint = function (p) {
	return this.mercatorPointToHeatmapPoint(CesiumHeatmap.wgs84ToMercator(p));
};

/*  Convert a mercator location to the corresponding heatmap location
 *
 *  p: a WGS84 location like {x: lon, y:lat}
 */
CHInstance.prototype.mercatorPointToHeatmapPoint = function (p) {
	var pn = {};

	pn.x = Math.round((p.x - this._xoffset) / this._factor + this._spacing);
	pn.y = Math.round((p.y - this._yoffset) / this._factor + this._spacing);
	pn.y = this.height - pn.y;

	return pn;
};

CHInstance.prototype._setWidthAndHeight = function (mbb) {
	this.width = ((mbb.east > 0 && mbb.west < 0) ? mbb.east + Math.abs(mbb.west) : Math.abs(mbb.east - mbb.west));
	this.height = ((mbb.north > 0 && mbb.south < 0) ? mbb.north + Math.abs(mbb.south) : Math.abs(mbb.north - mbb.south));
	this._factor = 1;

	if (this.width > this.height && this.width > CesiumHeatmap.defaults.maxCanvasSize) {
		this._factor = this.width / CesiumHeatmap.defaults.maxCanvasSize;

		if (this.height / this._factor < CesiumHeatmap.defaults.minCanvasSize) {
			this._factor = this.height / CesiumHeatmap.defaults.minCanvasSize;
		}
	} else if (this.height > this.width && this.height > CesiumHeatmap.defaults.maxCanvasSize) {
		this._factor = this.height / CesiumHeatmap.defaults.maxCanvasSize;

		if (this.width / this._factor < CesiumHeatmap.defaults.minCanvasSize) {
			this._factor = this.width / CesiumHeatmap.defaults.minCanvasSize;
		}
	} else if (this.width < this.height && this.width < CesiumHeatmap.defaults.minCanvasSize) {
		this._factor = this.width / CesiumHeatmap.defaults.minCanvasSize;

		if (this.height / this._factor > CesiumHeatmap.defaults.maxCanvasSize) {
			this._factor = this.height / CesiumHeatmap.defaults.maxCanvasSize;
		}
	} else if (this.height < this.width && this.height < CesiumHeatmap.defaults.minCanvasSize) {
		this._factor = this.height / CesiumHeatmap.defaults.minCanvasSize;

		if (this.width / this._factor > CesiumHeatmap.defaults.maxCanvasSize) {
			this._factor = this.width / CesiumHeatmap.defaults.maxCanvasSize;
		}
	}

	this.width = this.width / this._factor;
	this.height = this.height / this._factor;
};

//动态设置热力图的图层矩形区域
CHInstance.prototype._setBounds = function (bb) {
	this._mbounds = CesiumHeatmap.wgs84ToMercatorBB(bb);
	this._setWidthAndHeight(this._mbounds);

	this._options.radius = Math.round((this._options.radius) ? this._options.radius : ((this.width > this.height) ? this.width / CesiumHeatmap.defaults.radiusFactor : this.height / CesiumHeatmap.defaults.radiusFactor));

	this._spacing = this._options.radius * CesiumHeatmap.defaults.spacingFactor;
	this._xoffset = this._mbounds.west;
	this._yoffset = this._mbounds.south;

	this.width = Math.round(this.width + this._spacing * 2);
	this.height = Math.round(this.height + this._spacing * 2);

	this._mbounds.west -= this._spacing * this._factor;
	this._mbounds.east += this._spacing * this._factor;
	this._mbounds.south -= this._spacing * this._factor;
	this._mbounds.north += this._spacing * this._factor;

	this.bounds = CesiumHeatmap.mercatorToWgs84BB(this._mbounds);

	this._rectangle = Cesium.Rectangle.fromDegrees(this.bounds.west, this.bounds.south, this.bounds.east, this.bounds.north);
	
	this._container.setAttribute("style", "width: " + this.width + "px; height: " + this.height + "px; margin: 0px; display: none;");
	
	this._heatmap._renderer.setDimensions(this.width,this.height);
	//setDimensions(this.width,this.height)
}

/*  Set an array of heatmap locations
 *
 *  min:  the minimum allowed value for the data values
 *  max:  the maximum allowed value for the data values
 *  data: an array of data points in heatmap coordinates and values like {x, y, value}
 */
CHInstance.prototype.setData = function (min, max, data) {
	if (data && data.length > 0 && min !== null && min !== false && max !== null && max !== false) {
		this._heatmap.setData({
			min: min,
			max: max,
			data: data
		});

		this.updateLayer();
		return true;
	}

	return false;
};

/*  Set an array of WGS84 locations
 *
 *  min:  the minimum allowed value for the data values
 *  max:  the maximum allowed value for the data values
 *  data: an array of data points in WGS84 coordinates and values like { x:lon, y:lat, value }
 */
CHInstance.prototype.setWGS84Data = function (min, max, data) {
	if (data && data.length > 0 && min !== null && min !== false && max !== null && max !== false) {
		var convdata = [];

		for (var i = 0; i < data.length; i++) {
			var gp = data[i];

			var hp = this.wgs84PointToHeatmapPoint(gp);
			if (gp.value || gp.value === 0) {
				hp.value = gp.value;
			}

			convdata.push(hp);
		}

		return this.setData(min, max, convdata);
	}

	return false;
};

/*  Set whether or not the heatmap is shown on the map
 *
 *  s: true means the heatmap is shown, false means the heatmap is hidden
 */
CHInstance.prototype.show = function (s) {
	if (this._layer) {
		this._layer.show = s;
	}
};

/*  Update/(re)draw the heatmap
 */
CHInstance.prototype.updateLayer = function () {

	// only works with a Viewer instance since the cesiumWidget
	// instance doesn't contain an entities property
	if (CesiumHeatmap.defaults.useEntitiesIfAvailable && this._cesium.entities) {
		if (this._layer) {
			this._cesium.entities.remove(this._layer);
		}

		// Work around issue with material rendering in Cesium
		// provided by https://github.com/criis
		material = new Cesium.ImageMaterialProperty({
			image: this._heatmap._renderer.canvas,
		});
		if (Cesium.VERSION >= "1.21") {
			material.transparent = true;
		} else if (Cesium.VERSION >= "1.16") {
			material.alpha = 0.99;
		}

		this._layer = this._cesium.entities.add({
			show: true,
			rectangle: {
				coordinates: this._rectangle,
				material: material
			}
		});
	} else {
		if (this._layer) {
			this._cesium.scene.imageryLayers.remove(this._layer);
		}

		this._layer = this._cesium.scene.imageryLayers.addImageryProvider(CesiumHeatmap._getImageryProvider(this));
	}
};


/*  DON'T TOUCH:
 *
 *  heatmap.js v2.0.0 | JavaScript Heatmap Library: http://www.patrick-wied.at/static/heatmapjs/
 *
 *  Copyright 2008-2014 Patrick Wied <heatmapjs@patrick-wied.at> - All rights reserved.
 *  Dual licensed under MIT and Beerware license
 *
 *  :: 2014-10-31 21:16
 */
(function (a, b, c) {
	if (typeof module !== "undefined" && module.exports) {
		module.exports = c()
	} else if (typeof define === "function" && define.amd) {
		define(c)
	} else {
		b[a] = c()
	}
})("h337", this, function(){
	var HeatmapConfig = {
	  defaultRadius: 40,
	  defaultRenderer: 'canvas2d',
	  defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"},
	  defaultMaxOpacity: 1,
	  defaultMinOpacity: 0,
	  defaultBlur: .85,
	  defaultXField: 'x',
	  defaultYField: 'y',
	  defaultValueField: 'value', 
	  plugins: {}
	};
	var Store = (function StoreClosure() {

	  var Store = function Store(config) {
		this._coordinator = {};
		this._data = [];
		this._radi = [];
		this._min = 0;
		this._max = 1;
		this._xField = config['xField'] || config.defaultXField;
		this._yField = config['yField'] || config.defaultYField;
		this._valueField = config['valueField'] || config.defaultValueField;

		if (config["radius"]) {
		  this._cfgRadius = config["radius"];
		}
	  };

	  var defaultRadius = HeatmapConfig.defaultRadius;

	  Store.prototype = {
		// when forceRender = false -> called from setData, omits renderall event
		_organiseData: function(dataPoint, forceRender) {
			var x = dataPoint[this._xField];
			var y = dataPoint[this._yField];
			var radi = this._radi;
			var store = this._data;
			var max = this._max;
			var min = this._min;
			var value = dataPoint[this._valueField] || 1;
			var radius = dataPoint.radius || this._cfgRadius || defaultRadius;

			if (!store[x]) {
			  store[x] = [];
			  radi[x] = [];
			}

			if (!store[x][y]) {
			  store[x][y] = value;
			  radi[x][y] = radius;
			} else {
			  store[x][y] += value;
			}

			if (store[x][y] > max) {
			  if (!forceRender) {
				this._max = store[x][y];
			  } else {
				this.setDataMax(store[x][y]);
			  }
			  return false;
			} else{
			  return { 
				x: x, 
				y: y,
				value: value, 
				radius: radius,
				min: min,
				max: max 
			  };
			}
		},
		_unOrganizeData: function() {
		  var unorganizedData = [];
		  var data = this._data;
		  var radi = this._radi;

		  for (var x in data) {
			for (var y in data[x]) {

			  unorganizedData.push({
				x: x,
				y: y,
				radius: radi[x][y],
				value: data[x][y]
			  });

			}
		  }
		  return {
			min: this._min,
			max: this._max,
			data: unorganizedData
		  };
		},
		_onExtremaChange: function() {
		  this._coordinator.emit('extremachange', {
			min: this._min,
			max: this._max
		  });
		},
		addData: function() {
		  if (arguments[0].length > 0) {
			var dataArr = arguments[0];
			var dataLen = dataArr.length;
			while (dataLen--) {
			  this.addData.call(this, dataArr[dataLen]);
			}
		  } else {
			// add to store  
			var organisedEntry = this._organiseData(arguments[0], true);
			if (organisedEntry) {
			  this._coordinator.emit('renderpartial', {
				min: this._min,
				max: this._max,
				data: [organisedEntry]
			  });
			}
		  }
		  return this;
		},
		setData: function(data) {
		  var dataPoints = data.data;
		  var pointsLen = dataPoints.length;


		  // reset data arrays
		  this._data = [];
		  this._radi = [];

		  for(var i = 0; i < pointsLen; i++) {
			this._organiseData(dataPoints[i], false);
		  }
		  this._max = data.max;
		  this._min = data.min || 0;
		  
		  this._onExtremaChange();
		  this._coordinator.emit('renderall', this._getInternalData());
		  return this;
		},
		removeData: function() {
		  // TODO: implement
		},
		setDataMax: function(max) {
		  this._max = max;
		  this._onExtremaChange();
		  this._coordinator.emit('renderall', this._getInternalData());
		  return this;
		},
		setDataMin: function(min) {
		  this._min = min;
		  this._onExtremaChange();
		  this._coordinator.emit('renderall', this._getInternalData());
		  return this;
		},
		setCoordinator: function(coordinator) {
		  this._coordinator = coordinator;
		},
		_getInternalData: function() {
		  return { 
			max: this._max,
			min: this._min, 
			data: this._data,
			radi: this._radi 
		  };
		},
		getData: function() {
		  return this._unOrganizeData();
		}/*,

		  TODO: rethink.

		getValueAt: function(point) {
		  var value;
		  var radius = 100;
		  var x = point.x;
		  var y = point.y;
		  var data = this._data;

		  if (data[x] && data[x][y]) {
			return data[x][y];
		  } else {
			var values = [];
			// radial search for datapoints based on default radius
			for(var distance = 1; distance < radius; distance++) {
			  var neighbors = distance * 2 +1;
			  var startX = x - distance;
			  var startY = y - distance;

			  for(var i = 0; i < neighbors; i++) {
				for (var o = 0; o < neighbors; o++) {
				  if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
					if (data[startY+i] && data[startY+i][startX+o]) {
					  values.push(data[startY+i][startX+o]);
					}
				  } else {
					continue;
				  } 
				}
			  }
			}
			if (values.length > 0) {
			  return Math.max.apply(Math, values);
			}
		  }
		  return false;
		}*/
	  };


	  return Store;
	})();

	//核心渲染类 内置两个canvas
	var Canvas2dRenderer = (function Canvas2dRendererClosure() {
	  
	  var _getColorPalette = function(config) {
		var gradientConfig = config.gradient || config.defaultGradient;
		var paletteCanvas = document.createElement('canvas');
		var paletteCtx = paletteCanvas.getContext('2d');

		paletteCanvas.width = 256;
		paletteCanvas.height = 1;

		var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
		for (var key in gradientConfig) {
		  gradient.addColorStop(key, gradientConfig[key]);
		}

		paletteCtx.fillStyle = gradient;
		paletteCtx.fillRect(0, 0, 256, 1);

		return paletteCtx.getImageData(0, 0, 256, 1).data;
	  };

	  var _getPointTemplate = function(radius, blurFactor) {
		var tplCanvas = document.createElement('canvas');
		var tplCtx = tplCanvas.getContext('2d');
		var x = radius;
		var y = radius;
		tplCanvas.width = tplCanvas.height = radius*2;

		if (blurFactor == 1) {
		  tplCtx.beginPath();
		  tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
		  tplCtx.fillStyle = 'rgba(0,0,0,1)';
		  tplCtx.fill();
		} else {
		  var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
		  gradient.addColorStop(0, 'rgba(0,0,0,1)');
		  gradient.addColorStop(1, 'rgba(0,0,0,0)');
		  tplCtx.fillStyle = gradient;
		  tplCtx.fillRect(0, 0, 2*radius, 2*radius);
		}
		
		

		return tplCanvas;
	  };

	  var _prepareData = function(data) {
		var renderData = [];
		var min = data.min;
		var max = data.max;
		var radi = data.radi;
		var data = data.data;
		
		var xValues = Object.keys(data);
		var xValuesLen = xValues.length;

		while(xValuesLen--) {
		  var xValue = xValues[xValuesLen];
		  var yValues = Object.keys(data[xValue]);
		  var yValuesLen = yValues.length;
		  while(yValuesLen--) {
			var yValue = yValues[yValuesLen];
			var value = data[xValue][yValue];
			var radius = radi[xValue][yValue];
			//HGT 122 wtw : 不同数据不同半径
			// radius = radius*(500/value);
			// if( radius > 15   ){
				// radius = 15;
			// }else if(radius < 10){
				// radius = 10;
			// }
			// console.log("value: "+value+" radius: "+radius);
			//end
			renderData.push({
			  x: xValue,
			  y: yValue,
			  value: value,
			  radius: radius
			});
		  }
		}

		return {
		  min: min,
		  max: max,
		  data: renderData
		};
	  };

		//核心渲染类 内置两个canvas
	  function Canvas2dRenderer(config) {
		var container = config.container;
		var shadowCanvas = this.shadowCanvas = document.createElement('canvas');
		var canvas = this.canvas = config.canvas || document.createElement('canvas');
		var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0];

		var computed = getComputedStyle(config.container) || {};

		canvas.className = 'heatmap-canvas';

		this._width = canvas.width = shadowCanvas.width = +(computed.width.replace(/px/,''));
		this._height = canvas.height = shadowCanvas.height = +(computed.height.replace(/px/,''));

		//新建了两个 2d canvas
		this.shadowCtx = shadowCanvas.getContext('2d');
		this.ctx = canvas.getContext('2d');

		// @TODO:
		// conditional wrapper

		canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';

		container.style.position = 'relative';
		container.appendChild(canvas);
		//生成调色板
		this._palette = _getColorPalette(config);
		this._templates = {};

		this._setStyles(config);
	  };

	  Canvas2dRenderer.prototype = {
		renderPartial: function(data) {
		  this._drawAlpha(data);
		  this._colorize();
		},
		//核心渲染过程
		renderAll: function(data) {
		  // reset render boundaries
		  this._clear();
		  this._drawAlpha(_prepareData(data));
		  this._colorize();
		},
		_updateGradient: function(config) {
		  this._palette = _getColorPalette(config);
		},
		updateConfig: function(config) {
		  if (config['gradient']) {
			this._updateGradient(config);
		  }
		  this._setStyles(config);
		},
		setDimensions: function(width, height) {
		  this._width = width;
		  this._height = height;
		  this.canvas.width = this.shadowCanvas.width = width;
		  this.canvas.height = this.shadowCanvas.height = height;
		},
		_clear: function() {
		  this.shadowCtx.clearRect(0, 0, this._width, this._height);
		  this.ctx.clearRect(0, 0, this._width, this._height);
		},
		_setStyles: function(config) {
		  this._blur = (config.blur == 0)?0:(config.blur || config.defaultBlur);

		  if (config.backgroundColor) {
			this.canvas.style.backgroundColor = config.backgroundColor;
		  }

		  this._opacity = (config.opacity || 0) * 255;
		  this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255;
		  this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255;
		  this._useGradientOpacity = !!config.useGradientOpacity;
		},
		_drawAlpha: function(data) {
		  var min = this._min = data.min;
		  var max = this._max = data.max;
		  var data = data.data || [];
		  var dataLen = data.length;
		  // on a point basis?
		  var blur = 1 - this._blur;

		  while(dataLen--) {

			var point = data[dataLen];

			var x = point.x;
			var y = point.y;
			var radius = point.radius;
			// if value is bigger than max
			// use max as value
			var value = Math.min(point.value, max);
			var rectX = x - radius;
			var rectY = y - radius;
			var shadowCtx = this.shadowCtx;




			var tpl;
			if (!this._templates[radius]) {
			  this._templates[radius] = tpl = _getPointTemplate(radius, blur);
			} else {
			  tpl = this._templates[radius];
			}
			// value from minimum / value range
			// => [0, 1]
			shadowCtx.globalAlpha = (value-min)/(max-min);
			// shadowCtx.globalAlpha = 0.5;
			
			// shadowCtx.globalCompositeOperation = "lighter";
			shadowCtx.drawImage(tpl, rectX, rectY);

			// update renderBoundaries
			if (rectX < this._renderBoundaries[0]) {
				this._renderBoundaries[0] = rectX;
			  } 
			  if (rectY < this._renderBoundaries[1]) {
				this._renderBoundaries[1] = rectY;
			  }
			  if (rectX + 2*radius > this._renderBoundaries[2]) {
				this._renderBoundaries[2] = rectX + 2*radius;
			  }
			  if (rectY + 2*radius > this._renderBoundaries[3]) {
				this._renderBoundaries[3] = rectY + 2*radius;
			  }

		  }
		},
		_colorize: function() {
		  var x = this._renderBoundaries[0];
		  var y = this._renderBoundaries[1];
		  var width = this._renderBoundaries[2] - x;
		  var height = this._renderBoundaries[3] - y;
		  var maxWidth = this._width;
		  var maxHeight = this._height;
		  var opacity = this._opacity;
		  var maxOpacity = this._maxOpacity;
		  var minOpacity = this._minOpacity;
		  var useGradientOpacity = this._useGradientOpacity;

		  if (x < 0) {
			x = 0;
		  }
		  if (y < 0) {
			y = 0;
		  }
		  if (x + width > maxWidth) {
			width = maxWidth - x;
		  }
		  if (y + height > maxHeight) {
			height = maxHeight - y;
		  }

		  var img = this.shadowCtx.getImageData(x, y, width, height);
		  var imgData = img.data;
		  var len = imgData.length;
		  var palette = this._palette;


		  for (var i = 3; i < len; i+= 4) {
			var alpha = imgData[i];
			var offset = alpha * 4;


			if (!offset) {
			  continue;
			}

			var finalAlpha;
			if (opacity > 0) {
			  finalAlpha = opacity;
			} else {
			  if (alpha < maxOpacity) {
				if (alpha < minOpacity) {
				  finalAlpha = minOpacity;
				} else {
				  finalAlpha = alpha;
				}
			  } else {
				finalAlpha = maxOpacity;
			  }
			}

			imgData[i-3] = palette[offset];
			imgData[i-2] = palette[offset + 1];
			imgData[i-1] = palette[offset + 2];
			imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha*2;

		  }

		  img.data = imgData;
		  this.ctx.putImageData(img, x, y);

		  this._renderBoundaries = [1000, 1000, 0, 0];

		},
		getValueAt: function(point) {
		  var value;
		  var shadowCtx = this.shadowCtx;
		  var img = shadowCtx.getImageData(point.x, point.y, 1, 1);
		  var data = img.data[3];
		  var max = this._max;
		  var min = this._min;

		  value = (Math.abs(max-min) * (data/255)) >> 0;

		  return value;
		},
		getDataURL: function() {
		  return this.canvas.toDataURL();
		}
	  };


	  return Canvas2dRenderer;
	})();

	var Renderer = (function RendererClosure() {

	  var rendererFn = false;

	  if (HeatmapConfig['defaultRenderer'] === 'canvas2d') {
		rendererFn = Canvas2dRenderer;
	  }

	  return rendererFn;
	})();


	var Util = {
	  merge: function() {
		var merged = {};
		var argsLen = arguments.length;
		for (var i = 0; i < argsLen; i++) {
		  var obj = arguments[i]
		  for (var key in obj) {
			merged[key] = obj[key];
		  }
		}
		return merged;
	  }
	};
	// Heatmap Constructor
	var Heatmap = (function HeatmapClosure() {

	  var Coordinator = (function CoordinatorClosure() {

		function Coordinator() {
		  this.cStore = {};
		};

		//简单的事件通信机制
		Coordinator.prototype = {
		  on: function(evtName, callback, scope) {
			var cStore = this.cStore;

			if (!cStore[evtName]) {
			  cStore[evtName] = [];
			}
			cStore[evtName].push((function(data) {
				return callback.call(scope, data);
			}));
		  },
		  emit: function(evtName, data) {
			var cStore = this.cStore;
			if (cStore[evtName]) {
			  var len = cStore[evtName].length;
			  for (var i=0; i<len; i++) {
				var callback = cStore[evtName][i];
				callback(data);
			  }
			}
		  }
		};

		return Coordinator;
	  })();


	  var _connect = function(scope) {
		var renderer = scope._renderer;
		var coordinator = scope._coordinator;
		var store = scope._store;
		//renderPartial 渲染局部
		coordinator.on('renderpartial', renderer.renderPartial, renderer);
		//渲染所有
		coordinator.on('renderall', renderer.renderAll, renderer);
		//极端/极值变化
		coordinator.on('extremachange', function(data) {
		  scope._config.onExtremaChange &&
		  scope._config.onExtremaChange({
			min: data.min,
			max: data.max,
			gradient: scope._config['gradient'] || scope._config['defaultGradient']
		  });
		});
		store.setCoordinator(coordinator);
	  };


	  function Heatmap() {
		var config = this._config = Util.merge(HeatmapConfig, arguments[0] || {});
		this._coordinator = new Coordinator();
		if (config['plugin']) {
		  var pluginToLoad = config['plugin'];
		  if (!HeatmapConfig.plugins[pluginToLoad]) {
			throw new Error('Plugin \''+ pluginToLoad + '\' not found. Maybe it was not registered.');
		  } else {
			var plugin = HeatmapConfig.plugins[pluginToLoad];
			// set plugin renderer and store
			this._renderer = new plugin.renderer(config);
			
			this._store = new plugin.store(config);
		  }
		} else {
		  this._renderer = new Renderer(config);
		  tem = this._renderer;
		  this._store = new Store(config);
		}
		_connect(this);
	  };

	  // @TODO:
	  // add API documentation
	  Heatmap.prototype = {
		addData: function() {
		  this._store.addData.apply(this._store, arguments);
		  return this;
		},
		removeData: function() {
		  this._store.removeData && this._store.removeData.apply(this._store, arguments);
		  return this;
		},
		setData: function() {
		  this._store.setData.apply(this._store, arguments);
		  return this;
		},
		setDataMax: function() {
		  this._store.setDataMax.apply(this._store, arguments);
		  return this;
		},
		setDataMin: function() {
		  this._store.setDataMin.apply(this._store, arguments);
		  return this;
		},
		configure: function(config) {
		  this._config = Util.merge(this._config, config);
		  this._renderer.updateConfig(this._config);
		  this._coordinator.emit('renderall', this._store._getInternalData());
		  return this;
		},
		repaint: function() {
		  this._coordinator.emit('renderall', this._store._getInternalData());
		  return this;
		},
		getData: function() {
		  return this._store.getData();
		},
		getDataURL: function() {
		  return this._renderer.getDataURL();
		},
		getValueAt: function(point) {

		  if (this._store.getValueAt) {
			return this._store.getValueAt(point);
		  } else  if (this._renderer.getValueAt) {
			return this._renderer.getValueAt(point);
		  } else {
			return null;
		  }
		}
	  };

	  return Heatmap;

	})();

	var tem;
	// core
	var heatmapFactory = {
	  create: function(config) {
		return [new Heatmap(config),tem];
	  },
	  register: function(pluginKey, plugin) {
		HeatmapConfig.plugins[pluginKey] = plugin;
	  }
	};
	return heatmapFactory;
});